feat(slides): add Presentation.sections — slide CRUD Phase 4 (closes #11)#36
Merged
Conversation
) Phase 4 of issue #11 (slide CRUD epic). Implements PowerPoint Sections support — read, write, and round-trip the `p:extLst/p:ext{uri=521415D9-…}/p14:sectionLst` extension that PowerPoint uses to organize slides into named groups in the slide pane. The fork's slide CRUD epic is now complete: Phase 1 (#33), Phase 2 (#34), Phase 3 (#35), and Phase 4 (this PR) collectively shipped duplicate / delete / reorder / copy-between-decks / sections across the originally listed sub-features. New public API -------------- - `Presentation.sections` → `_Sections` collection. - `_Sections` is sequence-like: `__len__`, `__iter__`, `__getitem__`, `index()`. Adds: * `add_section(name, after=None) -> Section` — append, or insert immediately after an existing section when `after` is given. * `remove(section)` — drop section; cleans up the wrapping `p14:sectionLst` / `p:ext` / `p:extLst` chain when the last section goes. - `Section` exposes: * `name` (read/write str) * `id` (read-only GUID-with-braces, e.g. `{ABC-…}`) * `slides` (tuple of |Slide|, in section order) * `add_slide(slide)` — assign or move slide into this section (a slide can belong to at most one section) * `remove_slide(slide)` — drop a slide's section assignment; slide remains in the presentation. Membership invariants --------------------- Section membership references slides by their stable `p:sldId/@id` integer (NOT by `r:id` and NOT by position), so `Slides.move(...)`, indexed `add_slide(...)`, and `Slides.remove(...)` preserve assignment without any extra plumbing. Removed slides become orphan ids in the section's `p14:sldIdLst`; `Section.slides` silently skips them on read but the XML round-trips them untouched (deliberate — matching python-pptx's "preserve foreign data" doctrine). PowerPoint compatibility ------------------------ - Empty sections emit `<p14:sldIdLst/>` so all PowerPoint versions treat them consistently (some interpret an omitted `sldIdLst` as "all unsectioned slides," which is not what we mean). - Section ids generated as `{<UPPER-CASE-GUID>}` matching PowerPoint's wire shape. - Foreign `p:extLst/p:ext` siblings (`p15:*`, modification tracking, etc.) round-trip untouched — the section ext lives alongside, not in place of. Internal additions ------------------ - New `pptx/sections.py` module hosting `Section` and `_Sections` proxy classes. - `pptx/oxml/presentation.py` extended with `CT_PresentationExtensionList`, `CT_PresentationExtension`, `CT_SectionList`, `CT_Section`, `CT_SectionSlideIdList`, `CT_SectionSlideId`, plus `SECTION_LIST_EXT_URI` constant and `CT_Presentation.{section_list, get_or_add_section_list, remove_section_list}` helpers. - `pptx/oxml/ns.py` registers the `p14` prefix (`http://schemas.microsoft.com/office/powerpoint/2010/main`). - `pptx/oxml/__init__.py` registers the new element classes. - `p:extLst` added as a successor entry on the existing ZeroOrOne `sldMasterIdLst`/`sldIdLst`/`sldSz` slots so insert ordering is correct. Tangential test-infra fix ------------------------- `tests/unitutil/cxml.py` namespace-prefix grammar widened from `Word(alphas)` to `Word(alphas, alphanums)` so test fixtures can address `p14:`, `w14:`, `o15:`, etc. Local-name grammar already supported alphanums. Test coverage ------------- - 56 new unit tests in `tests/test_sections.py` covering oxml helpers, the `Section`/`_Sections` proxies, and 8 round-trip integration tests (no-sections baseline, names + membership, GUID preservation, membership-survives-move, removal pruning, orphan preservation, empty-section, unicode/XML-special name, unsectioned-on-section-remove, sibling-ext preservation). - 5 new behave scenarios in `features/sld-sections.feature` (default empty, add, slide membership, move-preserves-membership, remove cleans up extLst). - New `uat_slide_sections.py` (untracked per repo §6) builds a 6-slide / 3-section deck, demonstrates membership preservation through a slide move, and prints a structural read-back. Verification ------------ ``` $ python3 -m pytest tests/ -q | tail -3 3222 passed in 4.33s $ ruff check src tests | tail -3 All checks passed! $ python3 -m behave features/ --no-color | tail -3 999 scenarios passed, 0 failed, 0 skipped 3000 steps passed, 0 failed, 0 skipped ``` Closes #11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slide CRUD Phase 4 —
Presentation.sectionsCloses #11. Phase 4 of the slide CRUD epic. With this PR the seven sub-features in #11 are all shipped — Phase 1 (#33), Phase 2 (#34), Phase 3 (#35), and now Phase 4 here.
What this adds
Presentation.sections— a sequence-like collection over thep:extLst/p:ext{uri=521415D9-…}/p14:sectionLstextension that PowerPoint uses to organize slides into named groups in the slide pane.Public API
Presentation.sections→_Sections_Sections:__len__,__iter__,__getitem__,index(section),add_section(name, after=None),remove(section)Section:name(read/write),id(read-only GUID),slides(tuple),add_slide(slide),remove_slide(slide)Membership invariants
Section membership references slides by their stable
p:sldId/@idinteger (NOT byr:idand NOT by position), so:Slides.move(...)— section assignment survives reorder ✓Slides.remove(...)— surviving slides keep their assignment ✓Slides.add_slide(layout, index=N)— new slides are unsectioned, existing assignments untouched ✓Section.add_slide(slide)automatically removes it from any other section first.When a slide is removed from the presentation, its slide_id stays as an orphan in the section's
p14:sldIdLst.Section.slidessilently skips orphans on read; the XML round-trips them untouched. This deliberately matches python-pptx's "preserve foreign data" doctrine — we don't auto-prune.PowerPoint compatibility
<p14:sldIdLst/>so PowerPoint versions that interpret an omittedsldIdLstas "all unsectioned slides" don't pull surprises.{<UPPER-CASE-GUID>}matching PowerPoint's wire shape — e.g.{4080CFE4-F95C-449C-8898-95C81DD3D8B4}.p:extLst/p:extsiblings (p15:*modification tracking, etc.) round-trip untouched — the section ext lives alongside, not in place of.Internal additions
pptx/sections.pymodule hostingSectionand_Sectionsproxy classes.pptx/oxml/presentation.pyextended withCT_PresentationExtensionList,CT_PresentationExtension,CT_SectionList,CT_Section,CT_SectionSlideIdList,CT_SectionSlideId, plusSECTION_LIST_EXT_URIconstant andCT_Presentation.{section_list, get_or_add_section_list, remove_section_list}helpers.pptx/oxml/ns.pyregisters thep14prefix (http://schemas.microsoft.com/office/powerpoint/2010/main).pptx/oxml/__init__.pyregisters the new element classes.p:extLstadded as a successor entry on the existing ZeroOrOnesldMasterIdLst/sldIdLst/sldSzslots so insert ordering stays correct.Tangential test-infra fix
tests/unitutil/cxml.pynamespace-prefix grammar widened fromWord(alphas)toWord(alphas, alphanums)so test fixtures can addressp14:,w14:,o15:, etc. Local-name grammar already supported alphanums.Test coverage
tests/test_sections.py:CT_SectionList,CT_SectionSlideIdList, theCT_Presentationsection-list traversal helpers)Section/_SectionsproxiesSlides.remove, empty-section round-trip, unicode + XML-special chars in section name, unsectioned-on-section-remove, sibling-ext preservation.features/sld-sections.feature(default empty, add, slide membership, move-preserves-membership, remove cleans up extLst).uat_slide_sections.py(untracked per repo §6) builds a 6-slide / 3-section deck, demonstrates membership preservation through a slide move, and prints structural read-back. UAT signoff: ✓.Out of scope (follow-up issues)
These were considered and deliberately left for separate PRs to keep this one reviewable:
append_fromcross-deck section porting: when source has sections and isappend_from'd into a target, sections are not currently ported. The append_from machinery is Phase 3 territory; this is a worthy follow-up.p:custDataLsttags: orthogonal to sections (different extension), tracked separately.Verification